package roborally.program;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

import be.kuleuven.cs.som.annotate.*;

import roborally.exceptions.EndOfProgramException;
import roborally.exceptions.ProgramParsingError;
import roborally.model.Direction;
import roborally.model.EnergyAmount;
import roborally.model.Robot;
import roborally.program.commands.*;
import roborally.program.conditions.*;

/**
 * A class for dealing with programs, 
 * which can be executed for a robot.
 * 
 * @invar	The main command of each program must be a valid main command.
 * 			| canHaveAsMainCommand(getMainCommand())
 * @version 1.0
 * @author  Pieter Robberechts	(1e Bach Informatica)
 * @author	Jeroen Sanders		(1e Bach Informatica)
 */
public class Program {
	
	/**
	 * Initialize this new program with its main command.
	 * 	The main command is the first command of the program.
	 * 
	 * @param 	mainCommand
	 * 		  	The main command for this new program.
	 * @pre		The given main command must be a valid main command for a program.
	 * 			| canHaveAsMainCommand(mainCommand)
	 * @post	The main command of this robot is equal to the given main command.
	 * 			| new.getMainCommand() == mainCommand		
	 * 			
	 */
	public Program(Command mainCommand){
		assert( canHaveAsMainCommand(mainCommand) );
		this.mainCommand = mainCommand;
	}

	/**
	 * Variable registering the main command of this program.
	 */
	private final Command mainCommand;
	
	/**
	 * Return the main command of this program.
	 */
	@Basic @Raw @Immutable
	public Command getMainCommand() {
		return mainCommand;
	}

	/**
	 * Check whether the given command is a valid main command for a program.
	 * 
	 * @param 	mainCommand
	 * 			The main command to check for whether it is a valid one.
	 * @return	Return true if and only if the given main command is effective;
	 * 			else return false.
	 * 			| result == ( mainCommand != null )
	 */
	@Raw
	public boolean canHaveAsMainCommand(Command mainCommand) {
		return ( mainCommand != null );
	}
	
	/**
	 * Load the program stored at a given path.
	 * 
	 * @param 	path
	 * 			The path to the file which should be loaded.
	 * @return	Return a new program, which is the parsed version 
	 * 			of the string-based loaded program.
	 * 			| ...
	 * @throws	FileNotFoundException
	 * 			The given path is invalid.
	 * 			| ...
	 * @throws 	IOException
	 * 			An error occurred while reading the file.
	 * 			| ...
	 * @throws 	ProgramParsingError
	 * 			The loaded program doesn't start with a valid main command.
	 * 			(eg. the program starts with a condition instead of a command)
	 * 			| ...
	 */
	public static Program loadFromFile(String path) throws FileNotFoundException, IOException, ProgramParsingError {
		// Read every line of the file into a list
		FileReader fileReader = new FileReader(path);
		BufferedReader bufferedReader = new BufferedReader(fileReader);
		List<String> lines = new ArrayList<String>();
		String line = null;
		while ((line = bufferedReader.readLine()) != null) {
			lines.add(line);
		}
		bufferedReader.close();
		
		// Transform the list of lines to a string of a single line
		String program = "";
		for(String programLine : lines)
			program += programLine.trim();
		
		// Convert the string-based program to an object-based program
		lineCounter = 1;
		ProgramElement parsedProgram = parse(program);
		if(! (parsedProgram instanceof Command) )
			throw new ProgramParsingError("The program should begin with a valid command.");
		return new Program( (Command) parse(program) );
	}
	
	private static int lineCounter = 1;
	
	/**
	 * Analyze a string-based program and convert it into an object-based program.
	 * 
	 * @param 	program
	 * 			The string based program to analyze.
	 */
	private static ProgramElement parse(String program) {
		List<String> parsedCommand = splitBracketBlocks(program);
		
		if(parsedCommand.get(0).equals("while") && parsedCommand.size() == 3) {
			try {
				Condition condition = (Condition)parse(parsedCommand.get(1));
				Command command = (Command)parse(parsedCommand.get(2));
				return new Loop(lineCounter++, condition, command);
			} catch(ClassCastException e) {
				throw new ProgramParsingError("The valid syntax for a loop is (while (condition) (command))");
			}
		}else if(parsedCommand.get(0).equals("seq") && parsedCommand.size() > 1) {
			try {
				Command[] commandArray = new Command[parsedCommand.size()-1];
				int i = 0;
				for(String command : parsedCommand.subList(1, parsedCommand.size())) {
					commandArray[i] = (Command)parse(command);
					i++;
				}	
				return new Sequence(lineCounter++, commandArray);
			} catch (ClassCastException e) {
				throw new ProgramParsingError("The valid syntax for a sequence is (seq (command) (command) ...)");
			}
		}else if (parsedCommand.get(0).equals("if") && parsedCommand.size() == 4) {
			try {
				Condition condition = (Condition) parse(parsedCommand.get(1));
				Command firstCommand = (Command) parse(parsedCommand.get(2));
				Command secondCommand = (Command) parse(parsedCommand.get(3));
				return new Selection(lineCounter++, condition, firstCommand, secondCommand);
			} catch (ClassCastException e) {
				throw new ProgramParsingError("The valid syntax for a selection is (if (condition) (command) (command))");
			}
		}else if (parsedCommand.get(0).equals("move") && parsedCommand.size() == 1) {
			return new Move(lineCounter++);
		}else if (parsedCommand.get(0).equals("shoot") && parsedCommand.size() == 1) {
			return new Shoot(lineCounter++);
		}else if (parsedCommand.get(0).equals("pick-up-and-use") && parsedCommand.size() == 1) {
			return new PickUpAndUse(lineCounter++);
		}else if (parsedCommand.get(0).equals("turn clockwise") && parsedCommand.size() == 1) {
			return new Turn(lineCounter++, Direction.CLOCKWISE);	
		}else if (parsedCommand.get(0).equals("turn counter-clockwise") && parsedCommand.size() == 1) {
			return new Turn(lineCounter++, Direction.COUNTER_CLOCKWISE);		
		}else if (parsedCommand.get(0).equals("and") && parsedCommand.size() == 3) {
			try {
				Condition firstCondition = (Condition) parse(parsedCommand.get(1));
				Condition secondCondition = (Condition) parse(parsedCommand.get(2));
				return new And(lineCounter++, firstCondition, secondCondition);
			} catch (ClassCastException e) {
				throw new ProgramParsingError("The valid syntax for AND is (and (condition) (condition))");
			}
		}else if (parsedCommand.get(0).equals("or") && parsedCommand.size() == 3) {
			try {
				Condition firstCondition = (Condition) parse(parsedCommand.get(1));
				Condition secondCondition = (Condition) parse(parsedCommand.get(2));
				return new Or(lineCounter++, firstCondition, secondCondition);
			} catch (ClassCastException e) {
				throw new ProgramParsingError("The valid syntax for OR is (or (condition) (condition))");
			}
		}else if (parsedCommand.get(0).equals("not") && parsedCommand.size() == 2) {
			try {
				Condition condition = (Condition) parse(parsedCommand.get(1));
				return new Not(lineCounter++, condition);
			} catch (ClassCastException e) {
				throw new ProgramParsingError("The valid syntax for NOT is (not (condition))");
			}
		}else if (parsedCommand.get(0).equals("at-item") && parsedCommand.size() == 1) {
			return new AtItem(lineCounter++);
		}else if (parsedCommand.get(0).equals("can-hit-robot") && parsedCommand.size() == 1) {
			return new CanHitRobot(lineCounter++);
		}else if (parsedCommand.get(0).matches("energy-at-least \\d+") && parsedCommand.size() == 1) {
			try {
				Double energyInUnits = Double.parseDouble(parsedCommand.get(0).split(" ")[1]);
				if(!EnergyAmount.isValidAmountInUnits(energyInUnits))
					throw new ProgramParsingError("The value associated with energy-at-least is an invalid energy amount");
				EnergyAmount energy = new EnergyAmount(energyInUnits);
				return new EnergyAtLeast(lineCounter++, energy);
			}catch(NumberFormatException e) {
				throw new ProgramParsingError("The value associated with energy-at-least should be numeric.");
			}
		}else if (parsedCommand.get(0).equals("true") && parsedCommand.size() == 1) {
			return new True(lineCounter++);
		}else if (parsedCommand.get(0).equals("wall") && parsedCommand.size() == 1) {
			return new Wall(lineCounter++);
		}else {
			throw new ProgramParsingError("There is an unknown command/condition in your program.");
		}	
	}
	
	/**
	 * Split a string-based program in its components (command and arguments).
	 * 
	 * @param 	str
	 * 			The string-based program which should be split up.
	 */
	private static List<String> splitBracketBlocks(String str) {
		// Remove the outer brackets
		if(! (str.startsWith("(") && str.endsWith(")")) )
	    	throw new ProgramParsingError("The brackets in this program mismatch.");
	    String command = str.substring(1, str.length()-1);
	    
		// Filter the parts of the main expression
		List<String> parsedCommand = new ArrayList<String>();
		char[] chr = command.toCharArray();
		int pos = 0;
		// Select the keyword eg. while, seq, shoot,...
		int i = 0;
		while(i < chr.length && chr[i] != '(') i++;
		parsedCommand.add(command.substring(pos, i));
		pos = i;		
	
		// Select the (eventual) arguments
		int openBr = 0;
		for(int j = pos; j < chr.length; j++) {
			if(chr[j] == '(') {
				openBr++;
			}
			if(chr[j] == ')') {
				openBr--;
				if(openBr == 0) {
					parsedCommand.add(command.substring(pos, j+1));
					pos = j+1;
				}
			}
		}	
		return parsedCommand;
	}
	
	/**
	 * Save this program to a file at the given location.
	 * 
	 * @param 	path
	 * 			The location where the program should be stored.
	 * @effect	This program is saved to a file at the given path.
	 * @throws 	IOException  
	 * 			If the named file exists but is a directory rather than a regular file, 
	 * 			does not exist but cannot be created, or cannot be opened for any other reason.
	 * 			| ...
	 */
	public void saveToFile(String path) throws IOException {
		PrintWriter out = new PrintWriter(new FileWriter(path));
		out.print(prettyFormat());
		out.close();
	}
	
	/**
	 * Return a string-formatted version of this program.
	 * 
	 * @return	Return a pretty formatted string, representing the main command
	 * 			of this program.
	 * 			| result == getMainCommand().prettyFormat(0)
	 */
	public String prettyFormat() {
		return getMainCommand().prettyFormat(0);
	}
	
	/**
	 * Return the next basic command in this program for a robot, 
	 * given the line number of the previous executed command.
	 * 
	 * @param 	lineNb
	 * 			The line number of the last executed basic command, 
	 * 			this should be zero (or negative) when no previous command was executed yet.
	 * @param 	robot
	 * 			Search the next basic command for this robot.
	 * @return	Return the next basic command of this robot, starting from the
	 * 			line number of the previous command and taking into account 
	 * 			the current state of the robot.
	 * 			| result == getMainCommand().getNextBasicCommand(lineNb, robot)
	 * @throws	EndOfProgramException
	 * 			The program is finished.
	 * 			| getMainCommand().getNextBasicCommand(lineNb, robot) == null
	 */
	public BasicCommand getNextBasicCommand(int lineNb, Robot robot) throws EndOfProgramException{
		BasicCommand next = getMainCommand().getNextBasicCommand(lineNb, robot);
		if(next == null)
			throw new EndOfProgramException();
		return next;
	}
	
}
